package com.springboot.autoconfigure.oss.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.MD5;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.google.common.base.Splitter;
import com.google.common.collect.Maps;
import com.google.gson.JsonObject;
import com.springboot.autoconfigure.oss.config.OssConfigPropertis;
import com.springboot.autoconfigure.oss.config.Pan123OSSPropertis;
import com.springboot.autoconfigure.oss.model.FileInfo;
import com.springboot.autoconfigure.oss.model.FileOssOperationStatus;
import com.springboot.autoconfigure.oss.model.FileUploadStatus;
import com.springboot.autoconfigure.oss.service.DirectoryManagerService;
import com.springboot.autoconfigure.oss.service.FileNameService;
import com.springboot.autoconfigure.oss.service.OssApiService;
import com.springboot.autoconfigure.oss.util.DirectoryUtil;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.*;


@Slf4j
/**
 * 123云盘上传接口对接
 */
public class Pan123OssApiServiceImpl implements OssApiService<Pan123OSSPropertis>, DirectoryManagerService, InitializingBean{

    protected FileNameService fileNameService;
    protected Pan123OSSPropertis pan123OSSPropertis;
    protected OssConfigPropertis ossConfigPropertis;

    /**
     * 请求url
     */
    protected String domain;
    /**
     * 并发任务
     */
    ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    /**
     * 定时清空
     */
    protected ScheduledExecutorService freshToken;

    public Pan123OssApiServiceImpl(Pan123OSSPropertis pan123OSSPropertis, OssConfigPropertis ossConfigPropertis) {
        this.pan123OSSPropertis = pan123OSSPropertis;
        this.ossConfigPropertis = ossConfigPropertis;
        this.domain = pan123OSSPropertis.getDomain();


    }


    @Override
    public List getDirFile(String... dirName) {
        GetDirInfo.GetDirInfoBuilder getDirInfoBuilder = GetDirInfo.builder().parentFileId(Integer.parseInt(pan123OSSPropertis.getDirName())).page(1).limit(99).orderBy("file_id").orderDirection("asc");
        if (dirName!=null && dirName.length > 0){
            getDirInfoBuilder.parentFileId(Integer.parseInt(dirName[0]));
        }
        GetDirInfo build = getDirInfoBuilder.build();

        HttpResponse httpResponse = getRequest("/api/v1/file/list", JSONUtil.toJsonStr(build)).execute();
        if (!JSONUtil.toBean(httpResponse.body(),Map.class).get("code").equals(0)){
            log.error(httpResponse.body());
            return null;
        }
        log.info("getDirFile response:{}",httpResponse.body());
        GetDirInfoResponse getDirInfoResponse = JSONUtil.toBean(JSONUtil.toBean(httpResponse.body(),JSONObject.class).getStr("data"), GetDirInfoResponse.class);
        return getDirInfoResponse.getFileList();
    }


    /**
     * @param dirName 目录名称
     * @param parentDir 上级目录名称
     * @return
     */
    @Override
    public String mkdir(String dirName,String... parentDir) {
        HashMap<String, Object> initParam = getInitParam();
        initParam.put("name",dirName);
        if (null == parentDir || parentDir.length==0){
            initParam.put("parentID",Long.valueOf(pan123OSSPropertis.getDirName()));
        }else{
            initParam.put("parentID",Long.valueOf(parentDir[0]));
        }
        HttpResponse httpResponse = postRequest("/upload/v1/file/mkdir", JSONUtil.toJsonStr(initParam));
        return String.valueOf(JSONUtil.toBean(httpResponse.body(),JSONObject.class).getByPath("data.dirID"));
    }

    private HashMap<String, Object> getInitParam() {
        HashMap<String, Object> param = Maps.newHashMap();
        return param;
    }

    @Override
    public boolean rmdir(String... dirName) {
        HashMap<String, Object> initParam = getInitParam();
        initParam.put("fileIDs", Arrays.stream(dirName).toArray());
        HttpResponse httpResponse = postRequest("/api/v1/file/trash", JSONUtil.toJsonStr(initParam));
        return true;
    }

    @Override
    public List searchName(String dirName,String... parentName) {
        HashMap<String, Object> searchParam = getInitParam();
        searchParam.put("parentFileId",parentName[0]);
        searchParam.put("page",1);
        searchParam.put("limit",10);
        searchParam.put("orderBy","file_id");
        searchParam.put("orderDirection","desc");
        searchParam.put("searchData",dirName);
        HttpResponse execute = getRequest("/api/v1/file/list", JSONUtil.toJsonStr(searchParam)).execute();
        return (List)JSONUtil.getByPath(JSONUtil.toBean(execute.body(), JSONObject.class), "data.fileList");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        try {
            fileNameService = SpringUtil.getBean(FileNameService.class);
            if (StringUtils.isNotBlank(pan123OSSPropertis.getClientId()) && StringUtils.isNotBlank(pan123OSSPropertis.getClientSecret())){
                freshToken=new ScheduledThreadPoolExecutor(1);
                getToken();
                freshToken.scheduleWithFixedDelay(()->{
                    getToken();
                },1000,1000 * 60 * 15, TimeUnit.SECONDS);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public FileUploadStatus uploadFile(URL url) {
        return null;
    }

    @Override
    public FileUploadStatus uploadFile(URL url,String filePrix){
        return null;
    }

    /**
     * 线程的fileId
     */
    ThreadLocal<Long> fileId=new ThreadLocal<Long>();


    @Override
    public FileUploadStatus uploadFile(File file) {
        HashMap<String, String> localFileMappingCloudPathId = Maps.newHashMap();
        HashMap<String, String> cloudPathIdMappingLocalFile = Maps.newHashMap();
        boolean isDirectUpload=false;
        String fileName=null;
        if (file.isDirectory()) {
            // 1.本地目录上传到网盘;网盘文件夹初始化
            mkdirCloudPathIdByLocalDirLoop(file, localFileMappingCloudPathId, cloudPathIdMappingLocalFile);
            // 2.loopFileUpload
            List<File> dirFiles = FileUtil.loopFiles(file);
            for (File dirFile : dirFiles) {
                try {
                    singleFileUpload(dirFile, localFileMappingCloudPathId, isDirectUpload);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }else{
            fileName = singleFileUpload(file, localFileMappingCloudPathId, isDirectUpload);
        }
        // 启动直链
        try {
            openDirectUrl();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 获取直链
        String directUrl = getDirectUrl();
        FileUploadStatus suceess = FileUploadStatus.suceess();
        suceess.setFileName(fileName);
        suceess.setUrl(directUrl);
        suceess.setUrlIsPublic(true);
        return suceess;
    }

    private void mkdirCloudPathIdByLocalDirLoop(File file, HashMap<String, String> localFileMappingCloudPathId, HashMap<String, String> cloudPathIdMappingLocalFile) {
        String dirName = file.getAbsolutePath();
        TreeMap<String, Integer> integerStringTreeMap = DirectoryUtil.listDirectory(dirName, 1, Maps.newTreeMap());
        for (Map.Entry<String, Integer> integerStringEntry : integerStringTreeMap.entrySet()) {
            System.out.println(integerStringEntry.getKey() + " level->" + integerStringEntry.getValue());
            File dirFile = new File(integerStringEntry.getKey());
            String cloudPathId = null;
            if (integerStringEntry.getValue().equals(1)) {
                cloudPathId = pan123OSSPropertis.getDirName();
            } else {
                String parent = dirFile.getParent();
                cloudPathId = localFileMappingCloudPathId.get(parent);
            }
            try {
                // frequent create
                sleep();
                cloudPathId = mkdir(dirFile.getName(), cloudPathId);
            } catch (Exception e) {
                sleep();
                // have exists dir
                List list = searchName(dirFile.getName(), cloudPathId);
                for (Object o : list) {
                    JSONObject jsonObject = (JSONObject) o;
                    if (jsonObject.getStr("filename").equals(dirFile.getName())) {
                        cloudPathId = jsonObject.getStr("fileID");
                        break;
                    }
                }
                e.printStackTrace();
            }
            localFileMappingCloudPathId.put(integerStringEntry.getKey(), cloudPathId);
            cloudPathIdMappingLocalFile.put(cloudPathId, integerStringEntry.getKey());
            // TOOD cache
        }
    }

    private String singleFileUpload(File file, HashMap<String, String> localFileMappingCloudPathId, boolean isDirectUpload) {
        String fileName = fileNameService.getFileName(file);
        UpFileParam.UpFileParamBuilder upFileParamBuilder = getUpFileParamBuilder(file, fileName);
        if (StringUtils.isNotBlank(pan123OSSPropertis.getDirName())){
            upFileParamBuilder.parentFileID(Long.valueOf(pan123OSSPropertis.getDirName()));
        }
        // 文件夹上传
        if (isDirectUpload){
            upFileParamBuilder.parentFileID(Long.valueOf(localFileMappingCloudPathId.get(file.getParent())));
        }
        UpFileParam build =upFileParamBuilder.build();
        doUploadRequest(file, build);
        return fileName;
    }

    private UpFileParam.UpFileParamBuilder getUpFileParamBuilder(File file, String fileName) {
        UpFileParam.UpFileParamBuilder upFileParamBuilder = UpFileParam.builder().filename(fileName).etag(MD5.create().digestHex(file)).size(file.length());
        return upFileParamBuilder;
    }

    private void doUploadRequest(File file, UpFileParam build) {
        HttpResponse httpResponse = postRequest("/upload/v1/file/create", JSONUtil.toJsonStr(build));
        UpFileParamResponse upFileParamResponse = JSONUtil.toBean(JSONUtil.toBean(httpResponse.body(),JSONObject.class).getStr("data"), UpFileParamResponse.class);
        // 非极速秒传
        if (!upFileParamResponse.isReuse()){
            // 单个分片大小
            int sliceSize = upFileParamResponse.getSliceSize();
            // 文件总的分片个数
            int sliceNum =(int) Math.ceil((double) file.length() / sliceSize);
            try {
                FileInputStream fis = new FileInputStream(file);
                // 上传接口请求返回的分片大小
                byte[] buffer = new byte[sliceSize];
                int bytesRead;
                int partNumber = 1;
                CountDownLatch countDownLatch = new CountDownLatch(sliceNum);
                while ((bytesRead = fis.read(buffer)) != -1) {
                    if (bytesRead != buffer.length){
                        // #Bug fixed 最后一个分片大小
                        buffer = Arrays.copyOf(buffer, bytesRead);
                    }
                    // 分片并发上传
                    executorService.execute(new UploadPartTask(upFileParamResponse.getPreuploadID(),buffer,partNumber,countDownLatch));
                    buffer = new byte[sliceSize];
                    partNumber++;
                }
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 打印请求进度
                printUploadProcess(file, upFileParamResponse, sliceSize, sliceNum, buffer, partNumber);
                //确认上传完成
                confirmUploaded(upFileParamResponse);
            } catch (FileNotFoundException e ) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }else{
            // 当前上传请求的fileId
            fileId.set(upFileParamResponse.getFileID());
        }
    }

    private void sleep() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    class UploadPartTask implements Runnable{

        /**
         * 预上传id
         */
        private final String preuploadID;


        /**
         * 读取的大小
         */
        private final byte[] readBuffer;


        /**
         * 分片编号
         */
        private final int partUploadNumber;

        /**
         * 并发任务器
         */
        private final CountDownLatch countDownLatch;


        /**
         *
         * @param preuploadID
         * @param readBuffer
         * @param partUploadNumber
         * @param countDownLatch
         */
        public UploadPartTask(String preuploadID, byte[] readBuffer, int partUploadNumber,CountDownLatch countDownLatch) {
            this.preuploadID = preuploadID;
            this.readBuffer = readBuffer;
            this.partUploadNumber = partUploadNumber;
            this.countDownLatch= countDownLatch;
        }

        @Override
        public void run() {
            try {
                // 获取上传分片的url
                String presignedURL = getUploadUrl(preuploadID,partUploadNumber);
                // 上传分片
                HttpRequest.put(presignedURL).body(readBuffer).execute();
            } finally {
                countDownLatch.countDown();
            }

        }
    }



    /**
     * 开启直链
     * @return
     */
    private String openDirectUrl() {
        HashMap<String, Object> directUrlParam = getInitParam();
        directUrlParam.put("fileID", fileId.get());
        HttpResponse fileIDHttpResponse = postRequest("/api/v1/direct-link/enable", JSONUtil.toJsonStr(directUrlParam));
        log.info("直链:{}",fileIDHttpResponse.body());
        String directUrl = (String) JSONUtil.getByPath(JSONUtil.toBean(fileIDHttpResponse.body(), JSONObject.class), "data.filename");
        return directUrl;
    }


    /**
     * 直链获取
     * @return
     */
    private String getDirectUrl() {
        HashMap<String, Object> directUrlParam = getInitParam();
        directUrlParam.put("fileID", fileId.get());
        HttpRequest fileIDHttpRequest = getRequest("/api/v1/direct-link/url", JSONUtil.toJsonStr(directUrlParam));
        HttpResponse fileIDHttpResponse = fileIDHttpRequest.execute();
        log.info("直链:{}",fileIDHttpResponse.body());
        String directUrl = (String) JSONUtil.getByPath(JSONUtil.toBean(fileIDHttpResponse.body(), JSONObject.class), "data.url");
        return directUrl;
    }

    /**
     * 确认上传
     * @param upFileParamResponse
     */
    private void confirmUploaded(UpFileParamResponse upFileParamResponse) {
        JSONObject jsonObject = finishedUploadPart(upFileParamResponse.getPreuploadID());
        if (jsonObject.getBool("async")){// 异步确认接口
            // 异步轮询获取上传结果
            boolean flag=false;
            while (!flag){
                flag = asyncFinishedUploadConfirem(upFileParamResponse.getPreuploadID());
                try {
                    Thread.sleep(1100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }else{
            fileId.set(jsonObject.getLong("fileID"));
        }

    }


    /**
     * 打印上传进度
     * @param file
     * @param upFileParamResponse
     * @param sliceSize
     * @param sliceNum
     * @param buffer
     * @param partNumber
     */
    private void printUploadProcess(File file, UpFileParamResponse upFileParamResponse, int sliceSize, int sliceNum, byte[] buffer, int partNumber) {
        // 列举已上传的
        List<Object> uploadedPart = getUploadedPart(upFileParamResponse.getPreuploadID());
        log.info("当前分片的md5:{} - 分片编号:{} - 分片大小：{}",MD5.create().digestHex(buffer), partNumber, sliceSize);
        for (Object o : uploadedPart) {
            JSONObject jsonObject = JSONUtil.toBean(JSONUtil.toJsonStr(o), JSONObject.class);
            log.info("已上传的md5:{} - 分片编号:{} - 分片大小：{}",jsonObject.getStr("etag"),jsonObject.getInt("partNumber"),jsonObject.getInt("size"));
        }
        if (CollectionUtil.isNotEmpty(uploadedPart)){
            JSONObject jsonObject = JSONUtil.toBean(JSONUtil.toJsonStr(uploadedPart.get(uploadedPart.size() - 1)), JSONObject.class);
            Integer lastPartNumber = jsonObject.getInt("partNumber");
            double progress = (double) lastPartNumber / sliceNum;
            log.info("文件:{} \n 上传进度:{} - 总分片数：{} 当前分片编号：{}", file.getAbsolutePath(), String.format("%.2f%%", progress * 100), sliceNum,lastPartNumber);
        }
    }


    /**
     * 异步获取上传结果
     * @param preuploadID
     */
    private boolean asyncFinishedUploadConfirem(String preuploadID) {
        HashMap<String, Object> param =getInitParam();
        param.put("preuploadID",preuploadID);
        HttpResponse httpResponse = postRequest("/upload/v1/file/upload_async_result", JSONUtil.toJsonStr(param));
        JSONObject jsonObject = JSONUtil.toBean(httpResponse.body(), JSONObject.class);
        // 当前上传请求的fileId
        fileId.set(Long.valueOf(JSONUtil.getByPath((JSON)jsonObject,"data.fileID").toString()));
        return (boolean) JSONUtil.getByPath((JSON)jsonObject,"data.completed");
    }


    /**
     * 获取文件已分片上传的记录
     * @param preuploadID
     * @return
     */
    public List<Object> getUploadedPart(String preuploadID){
        HashMap<String, Object> param =getInitParam();
        param.put("preuploadID",preuploadID);
        HttpResponse httpResponse = postRequest("/upload/v1/file/list_upload_parts", JSONUtil.toJsonStr(param));
        JSONObject jsonObject = JSONUtil.toBean(httpResponse.body(), JSONObject.class);
        return (List<Object>) JSONUtil.getByPath((JSON)jsonObject,"data.parts");
    }

    /**
     * 完成上传
     */
    public JSONObject finishedUploadPart(String preuploadID){
        HashMap<String, Object> param =getInitParam();
        param.put("preuploadID",preuploadID);
        HttpResponse httpResponse = postRequest("/upload/v1/file/upload_complete", JSONUtil.toJsonStr(param));
        JSONObject jsonObject = JSONUtil.toBean(httpResponse.body(), JSONObject.class);
        log.info("完成上传:{}",jsonObject.toString());
        return (JSONObject) JSONUtil.getByPath((JSON)jsonObject,"data");
    }


    /**
     * 获取文件单个分片的上传路径
     * @param preuploadID
     * @param sliceNo
     * @return
     */
    private String getUploadUrl(String preuploadID, int sliceNo) {
        HashMap<String, Object> param =getInitParam();
        param.put("preuploadID",preuploadID+"");
        param.put("sliceNo",sliceNo);
        HttpResponse httpResponse = postRequest("/upload/v1/file/get_upload_url", JSONUtil.toJsonStr(param));
        JSONObject jsonObject = JSONUtil.toBean(httpResponse.body(), JSONObject.class);
        return JSONUtil.getByPath((JSON)jsonObject,"data.presignedURL").toString();
    }


    protected String token=null;
    protected String getToken() {
        if (token!=null){
            return token;
        }
        JsonObject jsonObject =new JsonObject();
        jsonObject.addProperty("clientID",pan123OSSPropertis.getClientId());
        jsonObject.addProperty("clientSecret",pan123OSSPropertis.getClientSecret());
        String getTokenUrl = "/api/v1/access_token";
        HttpResponse execute = postRequest(getTokenUrl, jsonObject.toString());
        String responseBody = execute.body();
        Object accessToken = JSONUtil.getByPath(JSONUtil.parse(responseBody), "data.accessToken");
        if (ObjectUtil.isEmpty(accessToken)){
            throw new RuntimeException("pan123 get token error");
        }
        token = accessToken.toString();
        return accessToken.toString();
    }

    private HttpResponse postRequest( String getTokenUrl,String jsonBody) {
        // 认证不打印日志
        if (!getTokenUrl.equals("/api/v1/access_token")){
            log.info("post request url:{}\nbody：{}",domain + getTokenUrl, jsonBody);
        }
        HttpRequest post = HttpRequest.post(domain + getTokenUrl).header("Platform", "open_platform")
                .body(jsonBody);
        if (StringUtils.isNotBlank(token)){
            post.header("Authorization",token);
        }
        HttpResponse execute = post.execute();
        log.info("post response {}",execute.body());
        // 异常
        if (!JSONUtil.toBean(execute.body(),JSONObject.class).getInt("code").equals(0)){
            throw new RuntimeException(JSONUtil.toBean(execute.body(),JSONObject.class).getStr("message"));
        }
        return execute;
    }

    private HttpRequest getRequest( String getTokenUrl,String jsonBody) {
        Map map = JSONUtil.toBean(jsonBody, Map.class);
        log.info("get request url:{}",domain + getTokenUrl+"?"+ HttpUtil.toParams(map));
        HttpRequest post = HttpRequest.get(domain + getTokenUrl+"?"+ HttpUtil.toParams(map)).header("Platform", "open_platform");
        if (StringUtils.isNotBlank(token)){
            post.header("Authorization",token);

        }
        return post;
    }


    @Override
    public List<FileInfo> getBucketSpaceFile(String prefix, int limit) {
        return null;
    }

    @Override
    public FileUploadStatus uploadAbsoulteFilePath(File file, String filePath) {
        FileUploadStatus suceess = FileUploadStatus.suceess();
        List<String> split = Splitter.on(StrUtil.DOT).splitToList(filePath);
        String cloudPathId = pan123OSSPropertis.getDirName();

        for (int i = 0; i < split.size(); i++) {
            try {
                // frequent create
                sleep();
                cloudPathId = mkdir(split.get(i), cloudPathId);
            } catch (Exception e) {
                sleep();
                // have exists dir
                List list = searchName(split.get(i), cloudPathId);
                for (Object o : list) {
                    JSONObject jsonObject = (JSONObject) o;
                    if (jsonObject.getStr("filename").equals(split.get(i))) {
                        cloudPathId = jsonObject.getStr("fileID");
                        break;
                    }
                }
                e.printStackTrace();
            }
        }

        UpFileParam.UpFileParamBuilder upFileParamBuilder = getUpFileParamBuilder(file, file.getName());
        upFileParamBuilder.parentFileID(Long.valueOf(cloudPathId));
        doUploadRequest(file, upFileParamBuilder.build());
        return suceess;
    }

    @Override
    public FileUploadStatus uploadFile(File file, String fileName) {
        return null;
    }

    @Override
    public FileUploadStatus uploadFileAndCallBack(File file, String callbackUrl) {
        return null;
    }

    @Override
    public List<String> findFile(String... fileNames) {
       List<String> resultUrl=new ArrayList<>();

        return resultUrl;
    }


    @Override
    public FileUploadStatus findFileByKey(String fileKey) {
        return null;
    }

    @Override
    public FileOssOperationStatus delFile(String... fileNames) {
         this.rmdir(fileNames);
         return FileOssOperationStatus.suceess();
    }


    /**
     * 获取目录
     */
    @Builder
    @Getter
    static class  GetDirInfo{
        private int parentFileId;
        private int page;
        private int limit;
        private String orderBy;
        private String orderDirection;
        private boolean trashed;
        private String searchData;
    }

    @Data
    static class  GetDirInfoResponse{
        private List<FileItem> fileList;
        @Data
        public static class FileItem {
            private int fileId;
            private String filename;
            private int type;
            private long size;
            private boolean etag;
            private int status;
            private int parentFileId;
            private String parentName;
            private int category;
            private int contentType;

        }
    }


    /**
     * 上传文件
     */
    @Builder
    @Getter
    static class UpFileParam{
        private long parentFileID;
        private String filename;
        private  String etag;
        private long size;
    }

    /**
     * 上传文件
     */
    @Data
    static class UpFileParamResponse{
        private long fileID;
        private String preuploadID;
        private boolean reuse;
        private int sliceSize;
    }
}
